﻿using System;
using System.IO;
using System.Text;
using System.Linq;
using System.ComponentModel;
using System.Net.Sockets;
using System.Collections.Generic;
using System.Collections.Concurrent;
using System.Net;
using System.Threading;
using psafth.Common.Extensions;
using System.Collections;

namespace psafth.AMB
{
    public class AMBCommunicator : BackgroundWorker
    {
        /// <summary>
        /// Works as a state-object
        /// </summary>
        private Socket _socket;

        /// <summary>
        /// Indicates if the socket shall remain open
        /// </summary>
        private bool _closeSocket = false;

        /// <summary>
        /// Stores the bytes read from the stream. If exception been thrown, this is saved in lastrecord.ba
        /// </summary>
        private byte[] _data = new byte[5000];

        // Declarations of AutoResetEvents
        private static AutoResetEvent connectedEvent = new AutoResetEvent(false);
        private static AutoResetEvent workerDone = new AutoResetEvent(false);

        /// <summary>
        /// The host to connect to
        /// </summary>
        public IPAddress RemoteAddress
        {
            get;
            set;
        }

        /// <summary>
        /// The port to connect on
        /// </summary>
        public int Port
        {
            get;
            set;
        }

        /// <summary>
        /// Constructs the object
        /// </summary>
        /// <param name="ipAddress">The host to connect to</param>
        /// <param name="port">The port to connect on</param>
        public AMBCommunicator(string remoteAddress, int port)
        {
            IPAddress ipAddress;

            if (!IPAddress.TryParse(remoteAddress, out ipAddress))
            {
                // Address is not an IPAddress
                try
                {
                    // Try to get the IP if it's a hostname
                    ipAddress = IPAddressHelper.GetIPAddressFromHostName(remoteAddress, port, false);
                }
                catch (Exception ex)
                {
                    // TODO: Handle the exception properly
                    throw ex;
                }
            }

            this.RemoteAddress = ipAddress;
            this.Port = port;
        }

        /// <summary>
        /// Closes the socket
        /// </summary>
        public void Disconnect()
        {
            if (!_socket.Connected)
            {
                // Nothing to disconnect
                return;
            }

            _socket.Shutdown(SocketShutdown.Both); // Make sure to do this
            _socket.BeginDisconnect(true, new AsyncCallback(OnDisconnected), null);
        }

        protected void Connect()
        {
            // Create a tcp socket
            Socket remoteSocket = new Socket(
                AddressFamily.InterNetwork,                     // IPv4
                SocketType.Stream,
                ProtocolType.Tcp
                );


            // Set up an endpoint
            IPEndPoint iep = new IPEndPoint(this.RemoteAddress, this.Port);

            // Connect
            remoteSocket.BeginConnect(iep, new AsyncCallback(OnConnected), remoteSocket);
        }

        /// <summary>
        /// Event that reset the state upon disconnect
        /// </summary>
        /// <param name="iar"></param>
        public void OnDisconnected(IAsyncResult iar)
        {
            // Set the state
            workerDone.Set();
        }

        /// <summary>
        /// Event that occures after connection.
        /// Starts listening to the Socket for streams.
        /// </summary>
        /// <param name="iar"></param>
        public void OnConnected(IAsyncResult iar)
        {
            try
            {
                if (iar.AsyncState is Socket)
                {
                    _socket = (Socket)iar.AsyncState;

                    _socket.EndConnect(iar);
                    _socket.BeginReceive(_data, 0, _data.Length, SocketFlags.None,
                                  new AsyncCallback(OnDataReceived), _socket);
                }
            }
            catch (Exception ex)
            {
                // TODO: Handle exception properly. Log?
                //throw;
            }
            finally
            {
                connectedEvent.Set();
            }
        }

        /// <summary>
        /// Send a stream to the tcp socket
        /// </summary>
        /// <param name="stream"></param>
        public void SendStream(byte[] stream)
        {
            if (_socket == null)
                throw new Exception("Socket has not been instanciated");

            _socket.BeginSend(stream, 0, stream.Length, SocketFlags.None,
                         new AsyncCallback(OnStreamSent), _socket);
        }

        /// <summary>
        /// Event after stream has been sent.
        /// After sent steam the socket is open for listening again.
        /// </summary>
        /// <param name="iar"></param>
        protected void OnStreamSent(IAsyncResult iar)
        {
            Socket remoteSocket = (Socket)iar.AsyncState;
            int sent = remoteSocket.EndSend(iar);

            remoteSocket.BeginReceive(_data, 0, _data.Length, SocketFlags.None,
                          new AsyncCallback(OnDataReceived), remoteSocket);
        }

        /// <summary>
        /// When a stream is received
        /// </summary>
        /// <param name="iar"></param>
        public virtual void OnDataReceived(IAsyncResult iar)
        {
            try
            {
                Socket remote = (Socket)iar.AsyncState;

                if (remote.Connected && !CancellationPending)
                {
                    int streamSize = remote.EndReceive(iar);

                    IEnumerable<byte> tmpData = _data.Take<byte>(streamSize);

                    List<IEnumerable<byte>> bufferedMessages = tmpData.Split<byte>(AMBMessage.EOR);

                    foreach (IEnumerable<byte> message in bufferedMessages)
                    {
                        if (message.First().Equals(AMBMessage.SOR) && message.Last().Equals(AMBMessage.EOR))
                        {
                            TransferObject obj = new TransferObject(message.ToArray());

                            if (obj.Message != null)
                            {
                                ReportProgress(0, obj);
                            }
                        }
                    }

                    #region Logging of each message

#if debug
                    List<string> strings = new List<string>();

                    strings.Add(string.Format("{0}|{1}", DateTime.Now.ToString(), BitConverter.ToString(tmpData.ToArray())));

                    System.IO.File.AppendAllLines("Log.txt", strings);
#endif

                    #endregion

                    // Open the socket for receiving again
                    _socket.BeginReceive(_data, 0, _data.Length, SocketFlags.None,
                                      new AsyncCallback(OnDataReceived), _socket);
                }
            }
            catch (SocketException)
            {
                // TODO: Handle exceptions properly
            }
            catch (Exception ex)
            {
                // TODO: Handle exceptions properly
                //throw;
            }
            finally
            {
                workerDone.Set();
            }
        }

        protected override void OnDoWork(DoWorkEventArgs e)
        {
            this.Connect();

            connectedEvent.WaitOne();

            if (e.Argument is byte[])
                SendStream((byte[])e.Argument);

            while (_socket.Connected && !CancellationPending)
            {
                workerDone.WaitOne();
            }

            this.Disconnect();
        }
    }
}
